//
// Author: Vladimir Migashko <migashko@gmail.com>, (C) 2011
//
// Copyright: See COPYING file that comes with this distribution
//

#ifndef HITLIST_USER_HITS_HPP
#define HITLIST_USER_HITS_HPP

#include "hit.hpp"
#include "hit_item.hpp"
// #include "tags.hpp"
#include "user_hits_func.hpp"
#include <list>
#include <map>
#include <deque>
#include <vector>
#include <algorithm>
#include <iostream>

/** @brief Управляет хитами конкретного пользователя
 * 
 * Ахтунг! Добавление хитов производиться в конец списка! Т.е. предполагается 
 * что хиты происходят последователно во времени. Нельзя добавлять хит со 
 * временем меньшим какого-либо хита, который был добавлен ранее.
 * 
 * Днем считается интервал от текущего момента до - 3600*24 секунд
 */
class user_hits
{
  typedef std::pair< user_id_t, span_t > second_hit;
  typedef std::deque< second_hit > hits_list;
  typedef std::vector< hit_item > hits_result;

public:
  
  /// Конструктор
  /// @param hit_interval ссылка на переменную, где храниться значения минимального интервала просмотра в секундах
  /// @param prepared_timelife максимальное время хранения кэша списков уникальных просмотров.
  /// @param days_in_month дней в месяце.
  user_hits(const time_t& hit_interval, const time_t&  prepared_timelife, const time_t&  days_in_month)
    : _hit_interval(hit_interval)
    , _prepared_timelife(prepared_timelife)
    , _days_in_month(days_in_month)
    , _month_prepared(false)
    , _day_prepared(false)
    , _hits_day(-1)
  {}

  /// Удалить все хиты старше 30 (days_in_month) дней. Если был удален хотя бы один хит, то очищается кэша списков уникальных просмотров.
  /// @return количество удаленных хитов.
  size_t remove_old()
  {
    time_t now = time(0);
    size_t count = _remove_old(now);
    if ( count > 0 || (_prepare_time + _prepared_timelife < now) )
      _reset_prepared();
    return count;
  }

  bool empty() const
  {
    return _hits.empty();
  }

  /// Общее количество просмотров за месяц
  /// @return размер списка
  size_t hits_month() const
  {
    return _hits.size();
  }

  /// Общее количество просмотров за день
  /// @return количество хитов от первого за текущий день ( now - 3600*24 ) до последнего
  size_t hits_day() const
  {
    if ( _hits_day == static_cast<size_t>(-1) ) 
      _hits_day = std::distance( _day_iterator( time(0) ), _hits.end()  );

    return _hits_day;
  }

  /// Количество уникальных просмотров за месяц
  /// @return размер списка кэша уникальных просмотров за месяц
  /// 
  /// Если список не сформирован, то формирует его. 
  /// Сформированный список хранится prepared_timelife.
  size_t hosts_month() const
  {
    if ( !_month_prepared )
      _prepare_month(time(0));
    return _month_result.size(); 
  }

  /// Количество уникальных просмотров за день
  /// @return размер списка кэша уникальных просмотров за день 
  /// 
  /// Если список не сформирован, то формирует его (также формирует список просмотров за месяц). 
  /// Сформированный список хранится prepared_timelife.
  size_t hosts_day() const
  {
    if ( !_day_prepared )
      _prepare_day( time(0) );
    return _day_result.size(); 
  }
  
  /// Добавить хит.
  /// @param second_id идентификатор пользователя который "посмотрел"
  /// @param hit_time  время просмотра (unix timespan)
  /// @return true если хит добавлен
  ///
  /// После добавления автоматически удаляются старые хиты и очищается кэш списков уникальных просмотров.
  ///
  /// Время просмотра должно быть больше или равно времени всех предыдущих хитов.
  /// В штатном режиме должно устанавляваться в time(0). Другое значение при востановлении из backup
  bool hit( user_id_t second_id, span_t hit_time  )
  {
    // time_t now = time(0);

    if ( !_hits.empty() )
    {
      /// Если время предыдущего хита больше вставляемого, то не добавляем
      if ( _hits.back().second > hit_time )
        return false;

      /// Если предыдущий хит от second_id был сделан меньше _hit_interval
      /// секунд назад то удаляем его
      time_t last = /*now*/hit_time - _hit_interval;
      hits_list::reverse_iterator ritr = _hits.rbegin();
      for ( ; ritr!=_hits.rend() && ritr->second >= last; ++ritr)
      {
        if ( ritr->first == second_id)
        {
          _hits.erase((++ritr).base());
          break;
        }
      }
    }

    /// записываем хит в конец
    _hits.push_back( std::make_pair(second_id, hit_time) );
    /// заодно удаляем старые хиты
    _remove_old(/*now*/time(0));
    /// кэш уникальных просмотров более не актуален
    _reset_prepared();
    return true;
  }
  
  /// Получить список хитов за текущий день ( time(0) - 3600*24 )
  /// @param offset
  /// @param limit
  /// @param hl 
  ///
  /// Копирует данные из списка кэша уникальных просмотров за день.
  /// Если список кэша не сформирован то формирует его.
  /// Кэш формируется без счетчиков количества просмотров. Поэтому
  /// перед копированием просматривает все элементы кэша, которые будут
  /// скопированы и если счетчик установлен в 0, то подсчитывает 
  /// количество просмотров и сохраняет в кэше.
  /// @see source_hits_day
  void day_hitlist( size_t offset, size_t limit, std::vector<hit_item>& hl ) const
  {
    if ( !_day_prepared )
      _prepare_day( time(0) );

    /// копируем данные из кэша _day_result, заодно посчитываем количество хитов 
    /// для каждого копируемого элемента (source_hits_day), если еще не подсчитан
    _hitlist(offset, limit, hl, _day_result, &user_hits::source_hits_day );
  }

  /// Получить список хитов за месяц
  /// @param offset
  /// @param limit
  /// @param hl 
  ///
  /// Копирует данные из списка кэша уникальных просмотров за месяц.
  /// Если список кэша не сформирован то формирует его.
  /// Кэш формируется без счетчиков количества просмотров. Поэтому
  /// перед копированием просматривает все элементы кэша, которые будут 
  /// скопированы и если счетчик установлен в 0, то подсчитывает 
  /// количество просмотров и сохраняет в кэше.
  /// @see source_hits_month
  void month_hitlist( size_t offset, size_t limit, std::vector<hit_item>& hl ) const
  {
    if ( !_month_prepared )
      _prepare_month(time(0));

    /// копируем данные из кэша _month_result, заодно посчитываем количество хитов
    /// для каждого копируемого элемента (source_hits_month), если еще не подсчитан
    _hitlist(offset, limit, hl, _month_result, &user_hits::source_hits_month );
  }

  /// Сколько раз пользователь "смотрел" за месяц
  /// @param second_id идентификатор пользователя который "смотрел"
  /// @return количиство хитов указанного пользователя за месяц
  size_t source_hits_month( user_id_t second_id ) const 
  {
    /*
    if ( !_month_prepared )
      _prepare_month( time(0) );
    /// посчитываем количество вхождений хита с заданным second_id
    return std::count_if(_month_result.begin(), _month_result.end(), f_hit_item_equal(second_id) );
    */
    return std::count_if(_hits.begin(), _hits.end(), f_hit_equal_second(second_id) );
  }

  /// Сколько раз пользователь "смотрел" за день 
  /// @param second_id идентификатор пользователя который "смотрел"
  /// @return количиство хитов указанного пользователя за день
  size_t source_hits_day( user_id_t second_id ) const
  {
    /*
    if ( !_day_prepared )
      _prepare_day(time(0));
    /// посчитываем количество вхождений хита с заданным second_id
    return std::count_if(_day_result.begin(), _day_result.end(), f_hit_item_equal(second_id) );
    */
    return std::count_if(_day_iterator(time(0)), _hits.end(), f_hit_equal_second(second_id) );
  }

private:

  /// Копирует уникальные просмотры за день или месяц и подсчитывает число просмотров для каждого элемента
  void _hitlist( size_t offset, size_t limit, std::vector<hit_item>& hl, hits_result& rl, size_t (user_hits::*source_hits)(user_id_t) const   ) const
  {
    if ( offset >= rl.size() )
      return;

    /// Определяем первый и последний элемент заданного диапазона
    hits_result::iterator beg = rl.begin();
    hits_result::iterator end = rl.end();
    std::advance(beg, offset);
    if ( std::distance(beg, end ) > static_cast<ptrdiff_t>(limit) )
      end = beg + limit;

    /// Проходим по всем элементам, которые будут скопированы и
    /// вычисляем hits
    hits_result::iterator itr = beg;
    for ( ;itr!=end; ++itr)
    {
      /// Вычисляем, только если не был вычислен ранее
      if ( itr->hits==0)
        itr->hits = (this->*source_hits)(itr->second_id);
    }

    hl.reserve( std::distance(beg, end) );
    /// копируем в конец
    std::copy( beg, end, std::back_inserter(hl) );
  }

  /// очищает кэш списков уникальных просмотров
  void _reset_prepared() const
  {
    _month_prepared = false;
    _day_prepared = false;
    _hits_day = -1;
    _month_result.clear();
    _day_result.clear();
    hits_result().swap(_month_result);
    hits_result().swap(_day_result);
    _prepare_time = 0;
  }

  /// Подготавливает кэш уникальных просмотров за месяц
  void _prepare_month(time_t now) const
  {
    _month_result.clear();
    _month_result.reserve(_hits.size());
    /// бля детский сад: unique работает правильно только со смежними 
    /// std::unique_copy(_hits.rbegin(), _hits.rend(), std::back_inserter(_month_result), f_hit_equal() );
    /// копируем уникальные хиты с конца (см. конструктор hit_item)
    std::reverse_copy( _hits.begin(), _hits.end(), std::back_inserter(_month_result) );

    hits_result::iterator beg = _month_result.begin();
    hits_result::iterator end = _month_result.end();
    hits_result::iterator end2 = end;
    for ( ;beg!=end; ++beg )
    {
      if ( std::distance(beg, end) > 0 )
        end = remove_if(beg+1, end, f_hit_item_equal(beg->second_id));
    }
    _month_result.erase(end, end2);
    _month_prepared = true;
    _prepare_time = now;
  }

  /// @return итератор на первый элемент за последние 24 часа в общем списке хитов
  hits_list::const_iterator _day_iterator( time_t now ) const
  {
    time_t last = now - 3600*24;

    return std::lower_bound(
      _hits.begin(), _hits.end(), 
      std::make_pair(0, last),
      f_hit_less() 
      );
  }

  /// Подготавливает кэш уникальных просмотров за день
  void _prepare_day(time_t now) const
  {
    _day_result.clear();

    /// Сначала делаем кэш уникальных за месяц
    if (!_month_prepared)
      _prepare_month(now);

    /// находим первый элемент за последние 24 часа 
    time_t last = now - 3600*24;
    hits_result::const_iterator beg = _month_result.begin();
    hits_result::const_iterator end  = _month_result.begin();

    hit_item hi;
    hi.time_stamp = last;
    /// Ищем от последнего элемента (т.к. отсортирован по убыванию)
    end = std::lower_bound(_month_result.rbegin(), _month_result.rend(), hi, f_hit_item_less() ).base();

    /// Копируем в кеш за день
    if (beg!=end)
    {
      _day_result.reserve( std::distance(beg, end) );
      std::copy(beg, end, std::back_inserter(_day_result) );
    }

    /// В кеше за месяц могут быть инициализированы hits, обнуляем их
    std::for_each( _day_result.begin(), _day_result.end(), f_hit_item_reset_hits() );

    _day_prepared = true;
    _prepare_time = now;
  }

  /// Удаляет просроченные хиты
  size_t _remove_old(time_t now)
  {
    time_t death_time = now - 3600*24*_days_in_month;
    size_t count = 0;
    for( ; !_hits.empty() && _hits.front().second < death_time; ++count )
      _hits.pop_front();
    return count;
  }

  hits_list _hits;
  const time_t& _hit_interval;
  const time_t& _prepared_timelife;
  const time_t& _days_in_month;
  mutable time_t _prepare_time;
  mutable bool _month_prepared;
  mutable bool _day_prepared;
  mutable hits_result _month_result;
  mutable hits_result _day_result;
  mutable size_t _hits_day;
};

#endif

